# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Utilities for bisection of object files.

This module contains a set of utilities to allow bisection between
two sets (good and bad) of object files. Mostly used to find compiler
bugs.
"""

from __future__ import print_function

import os
import shutil
import subprocess
import sys


def exec_and_return(execargs):
  """Execute process and return.

  Execute according to execargs and return immediatly. Don't inspect
  stderr or stdout.
  """
  return subprocess.call(execargs)


def makedirs(path):
  """Try to create directories in path."""
  try:
    os.makedirs(path)
  except os.error:
    if not os.path.isdir(path):
      raise


def get_obj_path(execargs):
  """Get the object path for the object file in the list of arguments."""
  try:
    i = execargs.index('-o')
  except ValueError:
    return "", ""

  obj_path = execargs[i+1]
  if not obj_path.endswith(('.o',)):
    # TODO: what suffixes do we need to contemplate
    # TODO: add this as a warning
    # TODO: need to handle -r compilations
    return "", ""

  return obj_path, os.path.join(os.getcwd(), obj_path)


def in_object_list(obj_name, list_filename):
  """Check if object file name exist in file with object list."""
  with open(list_filename, 'r') as list_file:
    for line in list_file:
      if line.strip() == obj_name:
        return True

  return False


def bisect_populate(execargs, bisect_dir, population_name):
  """Add necessary information to the bisect cache for the given execution.

  Extract the necessary information for bisection from the compiler
  execution arguments and put it into the bisection cache. This
  includes copying the created object object file, adding the object
  file path to the cache list and keeping a log of the execution.

  Args:
    execargs: compiler execution arguments.
    bisect_dir: bisection directory.
    population_name: name of the cache being populated (good/bad).
  """
  population_dir = os.path.join(bisect_dir, population_name)
  makedirs(population_dir)
  with open(os.path.join(population_dir, '_POPULATE_LOG'), 'a') as compile_log:
    compile_log.write('cd %s; %s\n' % (os.getcwd(), ' '.join(execargs)))

  obj_path, obj_full_path = get_obj_path(execargs)
  if not obj_path:
    return

  bisect_path = population_dir + obj_full_path
  bisect_path_dir = os.path.dirname(bisect_path)
  makedirs(bisect_path_dir)

  try:
    os.path.exists(obj_path) and shutil.copy2(obj_path, bisect_path)
  except Exception:
    print('Could not populate bisect cache', file=sys.stderr)
    raise

  with open(os.path.join(population_dir, '_LIST'), 'a') as object_list:
    object_list.write('%s\n' % obj_full_path)


def bisect_triage(execargs, bisect_dir):
  """Use object object file from appropriate cache (good/bad).

  Given a populated bisection directory, use the object file saved
  into one of the caches (good/bad) according to what is specified
  in the good/bad sets. The good/bad sets are generated by the
  high level binary search tool.

  Args:
    execargs: compiler execution arguments.
    bisect_dir: populated bisection directory.
  """
  obj_path, obj_full_path = get_obj_path(execargs)
  if not obj_path:
    return

  good_cache = os.path.join(bisect_dir, 'good')
  bad_cache = os.path.join(bisect_dir, 'bad')
  if not os.path.exists(good_cache) or not os.path.exists(bad_cache):
    raise ValueError('BISECT_GOOD_CACHE/BISECT_BAD_CACHE does not exist')

  good_set = os.path.join(bisect_dir, 'GOOD_SET')
  bad_set = os.path.join(bisect_dir, 'BAD_SET')
  if not os.path.exists(good_set) or not os.path.exists(bad_set):
    raise ValueError('BISECT_GOOD_SET/BISECT_BAD_SET not found')

  if in_object_list(obj_full_path, good_set):
    src_cache = good_cache
  elif in_object_list(obj_full_path, bad_set):
    src_cache = bad_cache
  else:
    raise AssertionError('Error: object not listed on good or bad sets %s'
                         % obj_full_path)

  bisect_path = src_cache + obj_full_path
  with open(os.path.join(bisect_dir, '_TRIAGE_LOG'), 'a') as triage_log:
    try:
      # TODO: copystat?
      shutil.copyfile(bisect_path, obj_path)
      triage_log.write('Using %s set for %s\n' % (src_cache, obj_full_path))
    except Exception:
      print('Could not copy from cache', file=sys.stderr)
      raise


def bisect_driver(bisect_stage, execargs):
  """Call appropriate bisection stage according to value in bisect_stage."""
  bisect_dir = os.environ.get('BISECT_DIR', '/tmp/sysroot_bisect')
  if bisect_stage == 'POPULATE_GOOD':
    bisect_populate(execargs, bisect_dir, 'good')
  elif bisect_stage == 'POPULATE_BAD':
    bisect_populate(execargs, bisect_dir, 'bad')
  elif bisect_stage == 'TRIAGE':
    bisect_triage(execargs, bisect_dir)
  else:
    raise ValueError('wrong value for BISECT_STAGE: %s' % bisect_stage)
